package it.battlehorse.scripting;

import static it.battlehorse.scripting.IScriptingConstants.EL_REQUIRED_OBJECT;
import static it.battlehorse.scripting.IScriptingConstants.EL_SCRIPT;
import static it.battlehorse.scripting.IScriptingConstants.ID_SCRIPTED_CONTRIBUTION;

import it.battlehorse.rcp.sl.annot.Injected;
import it.battlehorse.rcp.sl.annot.Serviceable;
import it.battlehorse.scripting.registry.IScript;
import it.battlehorse.scripting.registry.IScriptingRegistry;

import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

import org.apache.commons.logging.Log;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExtension;
import org.eclipse.core.runtime.IExtensionPoint;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.ui.PlatformUI;


/**
 * Provides helper methods to perform various script operations
 * 
 * @author battlehorse
 *
 */
@Serviceable
public class ScriptSupport {
	
	private Log log ; 
	
	@Injected
	public void setLog(Log log) {
		this.log = log;
	} 
	
	/**
	 * Runs the given script without additional parameters
	 * 
	 * @param script the script
	 * @throws ScriptException if any sort of error occurs while preparing or running the script
	 */
	public void runScript(IScript script) throws ScriptException {
		runScript(script, null);
	}
			
	/**
	 * Runs the given script
	 * 
	 * @param script the script
	 * @param params optional script parameters
	 * @throws ScriptException if any sort of error occurs while preparing or running the script
	 */
	public void runScript(final IScript script, Map<String,Object> params) throws ScriptException {
		ScriptEngineManager sem = new ScriptEngineManager();
		/*for (ScriptEngineFactory f : sem.getEngineFactories()) {
			System.out.println(f.getEngineName());
			System.out.println(f.getExtensions());
		}*/
		final ScriptEngine engine = sem.getEngineByExtension(script.getExtension());
		if (engine != null) {
			
			// add script params
			if (params != null)
				for (String key : params.keySet()) 
					engine.put(key, params.get(key));	
			
			// add script objects ( if available )
			if (script.getId() != null) {
				Map<String,Object> objs = getScriptObjects(script.getId());
				for (String key : objs.keySet()) {
					engine.put(key, objs.get(key));
				}
			}
			
			if (script.isModal()) {			
				try {
					PlatformUI.getWorkbench().getProgressService().busyCursorWhile(new IRunnableWithProgress() {
						public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException {
							// this monitor will not honour the "cancel" request, 
							// since the script will execute as an atomic operation
							
							monitor.beginTask("Running script...", IProgressMonitor.UNKNOWN);
							try {
								engine.eval(script.getReader());
							}
							catch(ScriptException se) {
								throw new InvocationTargetException(se,"The script has raised an exception");
							}
							catch(IOException ioe) {
								throw new InvocationTargetException(ioe,"The script has raised an exception");
							}
								
							monitor.done();
						}
					});
				} catch (InvocationTargetException e) {
					Throwable  t = e;
					if (e.getCause() != null) {
						t = e.getCause();
					}
					log.error(t.getMessage(), t);	
					throw new ScriptException("The script has raised an exception");
					
				} catch (InterruptedException e) {
					Throwable  t = e;
					if (e.getCause() != null) {
						t = e.getCause();
					}
					log.error(t.getMessage(), t);
					throw new ScriptException("The script has raised an exception");
				}
			}
			else {
				try {
					engine.eval(script.getReader());
				}
				catch(IOException ioe) {
					throw new ScriptException(ioe);
				}
			}					
		}
		else {
			throw new ScriptException("Engine not found for extension:" + script.getExtension());
		}		
	}
	
	/**
	 * Runs the given script without additional parameters 
	 * 
	 * @param script the script body
	 * @param extension the script extension (used to identify the correct script engine to be used)
	 * @throws ScriptException if any sort of error occurs while preparing or running the script
	 */
	public void runScript(String script, String extension) throws ScriptException {
		runScript(script,extension,null,false);
	}
			
	/**
	 * Runs the given script
	 * 
	 * @param script the script body
	 * @param extension the script extension (used to identify the correct script engine to be used)
	 * @param params the optional script parameters
	 * @param modal decides whether the script will run inside a progress monitor or not
	 * @throws ScriptException if any sort of error occurs while preparing or running the script
	 */
	public void runScript(
			final String script, 
			final String extension, 
			Map<String,Object> params,
			final boolean modal) throws ScriptException {

		runScript(new IScript() {
			public String getExtension() {
				return extension;
			}
			
			public Reader getReader() {
				return new StringReader(script);
			}
			
			public String getURI() { return null; 	}
			
			public String getId() { return null; }
			
			public String getNamespace() { return null; }
			
			public boolean isModal() { return modal; }
		},params);
	}	
	
	/**
	 * Runs the given script without additional parameters 
	 * 
	 * @param scriptReader a reader which reads from the script body
	 * @param extension the script extension (used to identify the correct script engine to be used)
	 * @throws ScriptException if any sort of error occurs while preparing or running the script
	 */
	public void runScript(Reader scriptReader, String extension) throws ScriptException {
		runScript(scriptReader,extension,null,false);
	}
	
	/**
	 * Runs the given script
	 * 
	 * @param scriptReader a reader which reads from the script body
	 * @param extension the script extension (used to identify the correct script engine to be used)
	 * @param params the optional script parameters
	 * @param modal decides whether the script will run inside a progress monitor or not
	 * @throws ScriptException if any sort of error occurs while preparing or running the script
	 */
	public void runScript(
			final Reader scriptReader, 
			final String extension, 
			Map<String,Object> params,
			final boolean modal) throws ScriptException {
		
		runScript(new IScript() {
			public String getExtension() {
				return extension;
			}
			
			public Reader getReader() {
				return scriptReader;
			}
			
			public String getURI() { return null; 	}
			
			public String getId() { return null; }
			
			public String getNamespace() { return null; }
			
			public boolean isModal() { return modal; }
		},params);
		
	}	
	
	/**
	 * Returns a map of the registered script objects for the given script. The key of the map entries is
	 * the 'variableName' attribute of the required object, the value is an instance of the object.
	 * 
	 * @param scriptId the script id
	 * @return a map of the registered script objects for the given script
	 * @throws ScriptException if an error occurs while parsing the script registry
	 */
	public Map<String,Object> getScriptObjects(String scriptId) throws ScriptException {		
		Map<String,Object> scriptObjects = new HashMap<String,Object>();
		
		IExtensionPoint point = Platform.getExtensionRegistry().getExtensionPoint(ID_SCRIPTED_CONTRIBUTION);
		for (IExtension ext: point.getExtensions()) {						
			for (IConfigurationElement el : ext.getConfigurationElements()) {
				getRecursiveScriptObjects(el, scriptId, scriptObjects);
			}
		}
		
		return scriptObjects;
	}
	
	/* Recursively scans the registry looking for required script objects. If a required object is not found
	 * an exception is thrown
	 */
	private void getRecursiveScriptObjects(IConfigurationElement el, String scriptId, Map<String,Object> scriptObjs)
		throws ScriptException {
		
		if (EL_SCRIPT.equals(el.getName())) {
			if (scriptId != null && scriptId.equals(el.getAttribute("id"))) {
				for (IConfigurationElement requiredObject : el.getChildren(EL_REQUIRED_OBJECT) ) {
					String id = requiredObject.getAttribute("id");
					String variableName = requiredObject.getAttribute("variableName");
					String required = requiredObject.getAttribute("required");
					
					IScriptingRegistry registry = ScriptingActivator.getDefault().getScriptingRegistry();
					Object obj = registry.getScriptObject(id);
					if (obj == null) {
						if ("true".equalsIgnoreCase(required)) 
							throw new ScriptException("Required script object " + id + " is unavailable");
						else
							log.warn("Optional script object " + id + " is unavailable");
					}
					else {
						log.info("Script object " + id + " is available under name " + variableName);
						scriptObjs.put(variableName, obj);
					}
				}				
			}
		}
		else {
			for (IConfigurationElement child : el.getChildren())
				getRecursiveScriptObjects(child, scriptId, scriptObjs);
		}
	}
	
	/**
	 * Returns the supported script languages. This method delegates to the jsr-223 <code>ScriptManager</code>.
	 * 
	 * @return the supported script languages
	 */
	public List<Language> getSupportedLanguages() {
		ScriptEngineManager sem = new ScriptEngineManager();
		List<ScriptEngineFactory> list = sem.getEngineFactories();
		List<Language> result = new ArrayList<Language>();
		ScriptEngineFactory f;
		for (int i = 0; i < list.size(); i++) {
			f = list.get(i);
			result.add(new Language(f.getLanguageName(), f.getExtensions()));
			
		}		
		
		return result; 
	}
	
	/**
	 * Models a scripting language
	 * 
	 */
	public static class Language {
		
		private List<String> extension;
		
		private String name;
		
		/**
		 * Creates a new language
		 * @param name
		 * @param extension
		 */
		public Language(String name, List<String> extension) {
			this.name = name; this.extension = extension;
		}
		
		/**
		 * Return the language name (for example : ECMAScript )
		 * 
		 * @return the language name (for example : ECMAScript )
		 */
		public String getName() { return name; }
		
		/**
		 * Returns the list of extensions associated with this language
		 * 
		 * @return the list of extensions associated with this language
		 */
		public  List<String> getExtension() { return extension; }
		
	}
		
}
